{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# DeepSphere using SHREC17 dataset\n",
    "## Benchmark with Cohen method S2CNN[[1]](http://arxiv.org/abs/1801.10130) and Esteves method[[2]](http://arxiv.org/abs/1711.06721)\n",
    "\n",
    "Multi-class classification of 3D objects, using the interesting property of rotation equivariance.\n",
    "\n",
    "The 3D objects are projected on a unit sphere.\n",
    "\n",
    "Several features are collected:\n",
    "* projection ray length (from sphere border to intersection [0, 2])\n",
    "* cos/sin with surface normal\n",
    "* same features using the convex hull of the 3D object\n",
    "\n",
    "### Equiangular sampling scheme\n",
    "Use of the equiangular sampling scheme used by Cohen to prove the flexibility of DeepSphere"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 0.1 Load libs"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext autoreload\n",
    "%autoreload 2\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import shutil\n",
    "import sys\n",
    "sys.path.append('../..')\n",
    "\n",
    "os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"1\"  # change to chosen GPU to use, nothing if work on CPU\n",
    "\n",
    "import numpy as np\n",
    "import time\n",
    "import matplotlib.pyplot as plt\n",
    "import healpy as hp"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from deepsphere import models, experiment_helper, plot, utils\n",
    "from deepsphere.data import LabeledDatasetWithNoise, LabeledDataset\n",
    "import hyperparameters\n",
    "\n",
    "from load_shrec import fix_dataset, Shrec17Dataset, Shrec17DatasetCache, Shrec17DatasetTF"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 0.2 Define parameters"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bw = 64\n",
    "experiment = 'equiangular'\n",
    "experiment_type = 'CNN' # 'FCN'\n",
    "ename = '_'+experiment_type\n",
    "datapath = '../../../data/shrec17/' # localisation of the .obj files"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "noise_dataset = True    # use perturbed dataset (Cohen and Esteves do the same)\n",
    "augmentation = 1        # number of element per file (1 = no augmentation of dataset)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 Load dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# if datasets are already downloaded but not preprocessed\n",
    "fix = False\n",
    "if fix:\n",
    "    fix_dataset(datapath+'val_perturbed')\n",
    "    fix_dataset(datapath+'test_perturbed')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "download dataset if True, preprocess data and store it in npy files, and load it in a dataset object"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "download = False\n",
    "train_dataset = Shrec17DatasetTF(datapath, 'train', perturbed=noise_dataset, download=download, nside=bw, \n",
    "                                    augmentation=augmentation, nfile=None, experiment = experiment)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "val_dataset = Shrec17DatasetCache(datapath, 'val', perturbed=noise_dataset, download=download, nside=bw, \n",
    "                                 augmentation=1, nfile=None, experiment = experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# from tqdm import tqdm\n",
    "# data_iter = train_dataset.iter(32)\n",
    "# steps = int(train_dataset.N / 32)\n",
    "# for i in tqdm(range(steps)):\n",
    "#     next(data_iter)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# data_iter = val_dataset.iter(32)\n",
    "# steps = int(val_dataset.N / 32)\n",
    "# for i in tqdm(range(steps)):\n",
    "#     next(data_iter)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1.1 Preprocess the dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Shuffle the training dataset and print the classes distribution"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "nclass = train_dataset.nclass\n",
    "num_elem = train_dataset.N\n",
    "print('number of class:',nclass,'\\nnumber of elements:',num_elem)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Plot sphere images"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Show what the projection looks like for the first two features"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = train_dataset.get_tf_dataset(1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "from tqdm import tqdm\n",
    "\n",
    "#dataset = tf_dataset_file(datapath, dataset, file_pattern, 32, Nside, augmentation)\n",
    "data_next = dataset.make_one_shot_iterator().get_next()\n",
    "config = tf.ConfigProto()\n",
    "config.gpu_options.allow_growth = True\n",
    "steps = train_dataset.N \n",
    "cm = plt.cm.RdBu_r\n",
    "cm.set_under('w')\n",
    "with tf.Session(config=config) as sess:\n",
    "    sess.run(tf.global_variables_initializer())\n",
    "#     try:\n",
    "    for i in range(steps):\n",
    "#         print(i)\n",
    "        out = sess.run(data_next)\n",
    "#         print(out[0].shape)\n",
    "        img, label = out\n",
    "        im1 = img[0,:,0]\n",
    "        cmin = np.min(img[:,:,0])\n",
    "        cmax = np.max(img[:,:,0])\n",
    "        plt.imshow(im1.reshape((2*bw,2*bw)), cmap=cm, vmin = cmin, vmax = cmax)\n",
    "#         print(label)\n",
    "        break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 Classification using DeepSphere"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use of the Dataset object used for other DeepSphere experiments"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "EXP_NAME = 'shrec17_equiangular_{}aug_{}bw{}'.format(augmentation, bw, ename)\n",
    "#EXP_NAME = \"shrec17_40sim_32sides_0noise_FCN\"\n",
    "#EXP_NAME = 'plop'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Load model with hyperparameters chosen.\n",
    "For each experiment, a new EXP_NAME is chosen, and new hyperparameters are store.\n",
    "All informations are present 'DeepSphere/Shrec17/experiments.md'\n",
    "The fastest way to reproduce an experiment is to revert to the commit of the experiment to load the correct files and notebook"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Adding a layer in the fully connected can be beneficial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "params = hyperparameters.get_params_shrec17_equiangular(num_elem, EXP_NAME, nclass, architecture=experiment_type)\n",
    "params[\"tf_dataset\"] = train_dataset.get_tf_dataset(params[\"batch_size\"])\n",
    "model = models.deepsphere(**params)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "shutil.rmtree('summaries/{}/'.format(EXP_NAME), ignore_errors=True)\n",
    "shutil.rmtree('checkpoints/{}/'.format(EXP_NAME), ignore_errors=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Find a correct learning rate"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# backup = params.copy()\n",
    "\n",
    "# params, learning_rate = utils.test_learning_rates(params, training.N, 1e-6, 1e-1, num_epochs=20)\n",
    "\n",
    "# shutil.rmtree('summaries/{}/'.format(params['dir_name']), ignore_errors=True)\n",
    "# shutil.rmtree('checkpoints/{}/'.format(params['dir_name']), ignore_errors=True)\n",
    "\n",
    "# model = models.deepsphere(**params)\n",
    "# _, loss_validation, _, _ = model.fit(training, validation)\n",
    "\n",
    "# params.update(backup)\n",
    "\n",
    "# plt.semilogx(learning_rate, loss_validation, '.-')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0.9 seems to be a good learning rate for SGD with current parameters"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.2 Train Network"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"the number of parameters in the model is: {:,}\".format(model.get_nbr_var()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "accuracy_validation, loss_validation, loss_training, t_step, t_batch = model.fit(train_dataset, val_dataset, use_tf_dataset=True, cache=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot.plot_loss(loss_training, loss_validation, t_step, params['eval_frequency'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Remarks"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# model.evaluate(train_dataset, None, cache='TF')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.evaluate(val_dataset, None, cache=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "probabilities,_ = model.probs(val_dataset, nclass, cache=True)\n",
    "# if augmentation>1:\n",
    "#     probabilities = probabilities.reshape((-1,augmentation,nclass))\n",
    "#     probabilities = probabilities.mean(axis=1)\n",
    "#     ids_val = ids_val[::repeat]\n",
    "predictions = np.argmax(probabilities, axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ids_val = val_dataset.get_ids()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# for every file, find every object with the same class, sorted by most relevance\n",
    "os.makedirs(os.path.join(datapath,'results_equiangular/val_perturbed'), exist_ok=True)\n",
    "for i,_id in enumerate(ids_val):\n",
    "    idfile = os.path.join(datapath,'results_equiangular/val_perturbed',_id)\n",
    "    # predictions batchxclass\n",
    "    # pred_class batch == predictions\n",
    "    retrieved = [(probabilities[j, predictions[j]], ids_val[j]) for j in range(len(ids_val)) if predictions[j] == predictions[i]]\n",
    "    retrieved = sorted(retrieved, reverse=True)\n",
    "    retrieved = [i for _, i in retrieved]\n",
    "    with open(idfile, \"w\") as f:\n",
    "        f.write(\"\\n\".join(retrieved))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "NaN appears if remove i==j case"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## test network"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_dataset = Shrec17DatasetCache(datapath, 'test', perturbed=noise_dataset, download=download, nside=bw, \n",
    "                              augmentation=augmentation, nfile=None, experiment=experiment)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.evaluate(test_dataset, None, cache=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ids_test = test_dataset.get_ids()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "labels_test = test_dataset.get_labels()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "probabilities,_ = model.probs(test_dataset, nclass, cache=True)\n",
    "if augmentation>1:\n",
    "    probabilities = probabilities.reshape((-1,augmentation,nclass))\n",
    "    probabilities = probabilities.mean(axis=1)\n",
    "    ids_test = ids_test[::augmentation]\n",
    "predictions = np.argmax(probabilities, axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "write to file"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# for every file, find every object with the same class, sorted by most relevance\n",
    "os.makedirs(os.path.join(datapath,'results_equiangular/test_perturbed'), exist_ok=True)\n",
    "for i, _id in enumerate(ids_test):\n",
    "    idfile = os.path.join(datapath,'results_equiangular/test_perturbed',_id)\n",
    "    # predictions batchxclass\n",
    "    # pred_class batch == predictions\n",
    "    retrieved = [(probabilities[j, predictions[j]], ids_test[j]) for j in range(len(ids_test)) if predictions[j] == predictions[i]]\n",
    "    retrieved = sorted(retrieved, reverse=True)\n",
    "    retrieved = [i for _, i in retrieved]\n",
    "    with open(idfile, \"w\") as f:\n",
    "        f.write(\"\\n\".join(retrieved))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Why not working?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def _print_histogram(nclass, labels_train, labels_val=None):\n",
    "    if labels_train is None:\n",
    "        return\n",
    "    import matplotlib.pyplot as plt\n",
    "    from collections import Counter\n",
    "    hist_train=Counter(labels_train)\n",
    "#         for i in range(self.nclass):\n",
    "#             hist_train.append(np.sum(labels_train == i))\n",
    "    labels, values = zip(*hist_train.items())\n",
    "    indexes = np.asarray(labels)\n",
    "#     miss = set(indexes) - set(labels)\n",
    "#     if len(miss) is not 0:\n",
    "#         hist_train.update({elem:0 for elem in miss})\n",
    "#     labels, values = zip(*hist_train.items())\n",
    "    width = 1\n",
    "    plt.bar(labels, values, width)\n",
    "    plt.title(\"labels distribution\")\n",
    "    #plt.xticks(indexes + width * 0.5, labels)\n",
    "    if labels_val is not None:\n",
    "        hist_val=Counter(labels_val)\n",
    "        plt.figure()\n",
    "        labels, values = zip(*hist_val.items())\n",
    "        indexes = np.asarray(labels)\n",
    "        width = 1\n",
    "        plt.bar(indexes, values, width)\n",
    "        plt.title(\"validation labels distribution\")\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_print_histogram(55, labels_test)\n",
    "_print_histogram(55, predictions)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Shrec projection outside"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from SHREC17.load_shrec import plot_healpix_projection, cache_healpix_projection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "im1=plot_healpix_projection('../data/shrec17/train_perturbed/000018.obj',128, outside=False, rotp=False, multiple=False, rot=(30,333,275))\n",
    "plt.savefig(\"./figures/lamp_sphere_side_000018.png\", bbox_inches='tight', transparent=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib import transforms\n",
    "from scipy.ndimage import rotate\n",
    "fig = plt.figure()\n",
    "ax = plt.subplot(111)\n",
    "im = plt.imread('./figures/000018.png')\n",
    "im = im[500:1600,1000:2800,:]\n",
    "im = rotate(im, -33)\n",
    "# print(im.shape)\n",
    "im = im[:1900,100:2000,:]\n",
    "circle = plt.Circle((im.shape[0]//2, im.shape[1]//2), 750, color='black', fill=False)\n",
    "ax.add_artist(circle)\n",
    "ax.plot([im.shape[0]//2, im.shape[1]//2], [im.shape[0]//2-750, im.shape[1]//2+750], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2-750, im.shape[1]//2-300], [im.shape[0]//2, im.shape[1]//2], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2+400, im.shape[1]//2+750], [im.shape[0]//2, im.shape[1]//2], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2-380/np.sqrt(2), im.shape[1]//2+750/np.sqrt(2)], [im.shape[0]//2-380/np.sqrt(2), im.shape[1]//2+750/np.sqrt(2)], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2-750/np.sqrt(2), im.shape[1]//2-450/np.sqrt(2)], [im.shape[0]//2-750/np.sqrt(2), im.shape[1]//2-450/np.sqrt(2)], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2-200/np.sqrt(2), im.shape[1]//2-750/np.sqrt(2)], [im.shape[0]//2+200/np.sqrt(2), im.shape[1]//2+750/np.sqrt(2)], 'k-', color='r', zorder=0)\n",
    "ax.plot([im.shape[0]//2+750/np.sqrt(2), im.shape[1]//2+150/np.sqrt(2)], [im.shape[0]//2-750/np.sqrt(2), im.shape[1]//2-150/np.sqrt(2)], 'k-', color='r', zorder=0)\n",
    "ax.imshow(im, zorder=100)\n",
    "ax.axis('off')\n",
    "# ax.get_yaxis().set_visible(False)\n",
    "# ax.get_xaxis().set_visible(False)\n",
    "plt.savefig(\"./figures/lamp_000018.svg\", bbox_inches='tight', transparent=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Creates map of shapes projected on the outside of the sphere\n",
    "* these maps are random part of the sphere graph, and cannot be used directly, as all graphs will be different"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "## generates new maps\n",
    "# cache_healpix_projection('../data/shrec17/', 'test', 32, repeat=3, outside='equator', rot=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_dataset = Shrec17DatasetCache(datapath, 'train', perturbed=noise_dataset, download=False, nside=32, \n",
    "                                  experiment='equator', augmentation=1, nfile=None)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_iter = train_dataset.iter(1)\n",
    "data, label = next(data_iter)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "im1 = data[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "im1[np.where(im1==0.)]=np.nan"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cm = plt.cm.RdBu_r\n",
    "cm.set_under('w')\n",
    "cmin = np.nanmin(im1)\n",
    "cmax = np.nanmax(im1)\n",
    "hp.orthview(im1, title='000003', nest=True, cmap=cm, min=cmin, max=cmax)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "indexes = np.where(np.invert(np.isnan(im1)))[0]\n",
    "npix = len(indexes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from deepsphere import utils"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g = utils.healpix_graph(nside=32, indexes=indexes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g.plot(vertex_size=10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g.plot_signal(im1[np.where(~np.isnan(im1))], edges=False, vertex_size=10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot(g.e[:16], 'o')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "g.compute_laplacian(\"normalized\")\n",
    "#g.compute_fourier_basis(recompute=True)\n",
    "g.set_coordinates(g.U[:,1:4])\n",
    "g.plot(vertex_size=10)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
